#include "process.h"
#include "global.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"    
#include "list.h"    
#include "tss.h"    
#include "interrupt.h"
#include "string.h"
#include "console.h"

extern void intr_exit(void);

/**
 * 构建用户进程、初始化其上下文
 * 该函数由 kernel_thread() 所调用
 * filename_ 是用户进程的名称
 */
void start_process(void* filename_) {
    void* function = filename_;
    struct task_struct* cur = running_thread(); // 进程是基于线程构建的
    cur -> self_kstack += sizeof(struct thread_stack); // 将 ESP 指向 intr_stack 栈底
    struct intr_stack* proc_stack = (struct intr_stack*) cur -> self_kstack;
    proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
    proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
    proc_stack -> gs = 0; // 用户态不允许操作显存，因此直接初始化为 0
    proc_stack -> ds = proc_stack -> es = proc_stack -> fs = SELECTOR_U_DATA;
    proc_stack -> eip = function; // 待执行的程序
    proc_stack -> cs = SELECTOR_U_CODE;
    proc_stack -> eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
    proc_stack -> esp = (void*) ((uint32_t) get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
    proc_stack -> ss = SELECTOR_U_DATA;
    asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(proc_stack) : "memory");
}

// 激活页表
void page_dir_activate(struct task_struct* p_thread) {
    /**
     * 为什么要重载线程的 CR3？
     * 我们知道进程才有独立的地址空间，而线程用的是线程间共享的同一套，按理说只需要重载进程就行，为什么线程也要呢？
     * 说实话我感觉没啥好说的...
     * 就是你线程用的是自己的，而进程们用的是各自独立的，你看线程和进程用的必然都不是同一套，你若要执行对应想线程或进程，是不是必须要重载 CR3 到自己所对应的页目录物理地址去？
     */
    uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录表物理地址

    if(p_thread -> pgdir != NULL) // 用户态进程自己的页目录表物理地址
        pagedir_phy_addr = addr_v2p((uint32_t) p_thread -> pgdir);

    // 更新页目录寄存器 CR3，使新页表生效
    asm volatile("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
}

// 激活线程或进程的页表，并且更新 TSS 中的 ESP0 为进程的特权级0 的栈
void process_activate(struct task_struct* p_thread) {
    ASSERT(p_thread != NULL);

    // 激活该进程或线程的页表
    page_dir_activate(p_thread); 

    // 若当前 p_thread 是内核线程，则不需要更新 ESP，因为其本身特权级就是0
    if(p_thread -> pgdir) 
        update_tss_esp(p_thread); // 更新用户进程的 ESP0，用于此进程被中断时恢复上下文
}

// 创建也目录表，将当前页表的 表示内核空间的 PDE 复制
// 成功则返回页目录的虚拟地址，否则返回 -1
uint32_t* create_page_dir(void) {
    // 用户进程的页表不能让用户直接访问到，所以在内核空间来申请
    uint32_t* page_dir_vaddr = get_kernel_pages(1);
    if(page_dir_vaddr == NULL) {
        console_put_str("create_page_dir: get_kernel_page failed!");
        return NULL;
    }

    /************* 先复制页表 *************/
    // page_dir_vaddr + 0x300 * 4 表示内核页目录的第 768 项
    memcpy((uint32_t*) ((uint32_t) page_dir_vaddr + 0x300 * 4), (uint32_t*) (0xfffff000 + 0x300 * 4), 1024);

    /************* 更新页目录地址 *************/
    uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t) page_dir_vaddr);
    // 页目录地址在页目录的最后一项，更新页目录地址为新的物理地址
    page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;

    return page_dir_vaddr;
}

// 创建用户进程的虚拟地址的位图
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
    user_prog -> userprog_vaddr.vaddr_start = USER_VADDR_START;
    uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
    user_prog -> userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
    user_prog -> userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
    bitmap_init(&user_prog -> userprog_vaddr.vaddr_bitmap);
}

/**
 * 创建用户进程
 * filename 用户进程地址
 * name 进程名
 */
void process_execute(void* filename, char* name) {
    struct task_struct* thread = get_kernel_pages(1); // 向内核物理内存池申请空间
    init_thread(thread, name, default_prio);
    create_user_vaddr_bitmap(thread);
    thread_create(thread, start_process, filename);
    thread -> pgdir = create_page_dir();
    block_desc_init(thread -> u_block_desc);

    enum intr_status old_status = intr_disable();
    ASSERT(!elem_find(&thread_ready_list, &thread -> general_tag));
    list_append(&thread_ready_list, &thread -> general_tag);

    ASSERT(!elem_find(&thread_all_list, &thread -> all_list_tag));
    list_append(&thread_all_list, &thread -> all_list_tag);
    intr_set_status(old_status);
}
